using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;

namespace Com.PhilChuang.Utils
{
	/// <summary>
	/// Signifies that the Property should be used in SimpleEqualsAndHashcode
	/// </summary>
	[AttributeUsage (AttributeTargets.Property | AttributeTargets.Field)]
	public class SimpleEqualsAndHashcodeAttribute : Attribute
	{
		private bool myUseForEquals = true;
		private bool myUseForHashCode = true;
		private bool myUseForClone = true;

		[SimpleEqualsAndHashcodeAttribute]
		public bool UseForEquals
		{
			get { return myUseForEquals; }
			set { myUseForEquals = value; }
		}
		[SimpleEqualsAndHashcodeAttribute]
		public bool UseForHashCode
		{
			get { return myUseForHashCode; }
			set { myUseForHashCode = value; }
		}
		[SimpleEqualsAndHashcodeAttribute]
		public bool UseForClone
		{
			get { return myUseForClone; }
			set { myUseForClone = value; }
		}

		public SimpleEqualsAndHashcodeAttribute ()
		{
		}

		public SimpleEqualsAndHashcodeAttribute (bool eq, bool hc, bool clone)
		{
			myUseForEquals = eq;
			myUseForHashCode = hc;
			myUseForClone = clone;
		}

		#region Equality / Cloning
		public override bool Equals (object obj)
		{
			return SimpleEqualsAndHashcode.Equals (this, obj);
		}

		public override int GetHashCode ()
		{
			return SimpleEqualsAndHashcode.GetHashCode (this);
		}

		public Object Clone ()
		{
			return SimpleEqualsAndHashcode.Clone (this);
		}
		#endregion
	}

	public class SimpleEqualsAndHashcode
	{
		static SimpleEqualsAndHashcode () { }
		private SimpleEqualsAndHashcode () { }

		internal static bool _myequals (object obj1, object obj2)
		{
			if (Object.ReferenceEquals (obj1, obj2)) return true;

			if (obj1 == null && obj2 == null) // both null
				return true;
			if (obj1 == null ^ obj2 == null) // just one is null
				return false;
			// none are null

			// specific handlers
			Type genKvpOpenType = typeof (KeyValuePair<,>);
			Type genListOpenType = typeof (List<>);
			Type ienumType = typeof (System.Collections.IEnumerable);
			Type obj1Type = obj1.GetType ();
			Type obj2Type = obj2.GetType ();
			if (!obj1Type.Equals (obj2Type)) // mismatched types
				return false;

			// handle certain cases right now
			if (obj1Type.Equals (typeof (String))) // otherwise handled by IEnumerable<Char>
			{
				return obj1.Equals (obj2);
			}

			if ((obj1Type.IsGenericType && obj1Type.GetGenericTypeDefinition ().Equals (genListOpenType))
				|| obj1 is System.Collections.IList)
			{ // handle IList, List<>
				if (((System.Collections.IList) obj1).Count == 0 && ((System.Collections.IList) obj2).Count == 0) // both empty
					return true;
				if (((System.Collections.IList) obj1).Count != ((System.Collections.IList) obj2).Count) // different sizes
					return false;
				System.Collections.IEnumerator ie1 = ((System.Collections.IList) obj1).GetEnumerator ();
				System.Collections.IEnumerator ie2 = ((System.Collections.IList) obj2).GetEnumerator ();

				try
				{
					while (ie1.MoveNext ())
					{
						Object sub1 = ie1.Current;
						if (!ie2.MoveNext ()) return false;
						Object sub2 = ie2.Current;
						if (!_myequals (sub1, sub2)) return false;
					}
					if (ie2.MoveNext ()) return false;
				}
				catch (Exception e)
				{
					Console.Out.WriteLine (e);
				}

				return true;
			}
			
			if (ienumType.IsAssignableFrom (obj1Type))
			{ // handle IEnumerable
				System.Collections.IEnumerator ie1 = ((System.Collections.IEnumerable) obj1).GetEnumerator ();
				System.Collections.IEnumerator ie2 = ((System.Collections.IEnumerable) obj2).GetEnumerator ();

				try
				{
					while (ie1.MoveNext ())
					{
						Object sub1 = ie1.Current;
						if (!ie2.MoveNext ()) return false;
						Object sub2 = ie2.Current;
						if (!_myequals (sub1, sub2)) return false;
					}
					if (ie2.MoveNext ()) return false;
				}
				catch (Exception e)
				{
					Console.Out.WriteLine (e);
				}

				return true;
			}
			
			if (obj1Type.IsGenericType && obj1Type.GetGenericTypeDefinition ().Equals (genKvpOpenType))
			{ // handle KVP

				if (!_myequals (obj1Type.GetProperty ("Key").GetValue (obj1, null), obj1Type.GetProperty ("Key").GetValue (obj2, null))) return false;
				if (!_myequals (obj1Type.GetProperty ("Value").GetValue (obj1, null), obj1Type.GetProperty ("Value").GetValue (obj2, null))) return false;

				return true;
			}
			
			if (obj1Type.IsArray && obj1Type.HasElementType)
			{ // handle <whatever>[]
				// TODO TEST!!!!
				Array arr1 = (Array) obj1;
				Array arr2 = (Array) obj2;
				if (arr1.Length != arr2.Length) return false;
				for (int i = 0; i < arr1.Length; i++)
					if (!_myequals (arr1.GetValue (i), arr2.GetValue (i))) return false;

				return true;
			}
			
			return obj1.Equals (obj2);
		}

		internal static void _hc (object obj, ref int hc, ref int idx)
		{
			int objHc = obj != null ? obj.GetHashCode () : idx;
			if (hc == 1)
			{
				hc = objHc != 0 ? objHc : idx;
			}
			else
			{
				//hc = (int) Math.Pow (hc, objHc != 0 ? objHc : idx); // this returns 0 instead of overflowing
				//hc ^= objHc != 0 ? objHc : idx; // WRONG THIS IS XOR NOT POWER OF
				//long longHc = (long) hc * (Math.Abs (objHc) > 1 ? objHc : idx); // calculate in longs
				long longHc = hc;
				if (Math.Abs (objHc) > 1)
					longHc *= objHc;
				else
					for (int i = 0; i < idx; i++)
						longHc *= longHc;
				hc = (int) (longHc & 0xFFFFFFFF); // use the bottom 32 bits
				if (hc == 0) hc = (int) (longHc >> 32); // if 0, use the top 32 bits
				if (hc == 0) hc = idx; // if still 0, use the indexer
			}
			idx++;
		}

		/// <summary>
		/// Compares objects based on properties decorated with SimpleEqualsAndHashcodeAttribute
		/// </summary>
		/// <param name="obj1"></param>
		/// <param name="obj2"></param>
		/// <returns></returns>
		public new static bool Equals (Object obj1, Object obj2)
		{
			if (Object.ReferenceEquals (obj1, obj2)) return true;

			if (obj1 == null && obj2 == null) // both null
				return true;
			if (obj1 == null ^ obj2 == null) // just one is null
				return false;

			Type type1 = obj1.GetType ();
			Type type2 = obj2.GetType ();
			if (!type1.Equals (type2)) return false;

			PropertyInfo[] props = type1.GetProperties (BindingFlags.Public | BindingFlags.Instance);
			foreach (PropertyInfo prop in props)
			{
				Object[] attrs = prop.GetCustomAttributes (typeof (SimpleEqualsAndHashcodeAttribute), true);
				if (attrs.Length != 0)
				{
					SimpleEqualsAndHashcodeAttribute attr = (SimpleEqualsAndHashcodeAttribute) attrs[0];
					if (!attr.UseForEquals) continue;
					Object val1 = prop.GetValue (obj1, null);
					Object val2 = prop.GetValue (obj2, null);
					if (!_myequals (val1, val2)) return false;
				}
			}

			return true;
		}

		/// <summary>
		/// Gets the hash code based on properties decorated with SimpleEqualsAndHashcodeAttribute 
		/// </summary>
		/// <param name="obj"></param>
		/// <returns></returns>
		public static int GetHashCode (Object obj)
		{
			if (obj == null) return 0;

			int hc = 1;
			int idx = 2;
			Type type = obj.GetType ();

			PropertyInfo[] props = type.GetProperties (BindingFlags.Public | BindingFlags.Instance);
			foreach (PropertyInfo prop in props)
			{
				Object[] attrs = prop.GetCustomAttributes (typeof (SimpleEqualsAndHashcodeAttribute), true);
				if (attrs.Length != 0)
				{
					SimpleEqualsAndHashcodeAttribute attr = (SimpleEqualsAndHashcodeAttribute) attrs[0];
					if (!attr.UseForHashCode) continue;
					Object val = prop.GetValue (obj, null);
					_hc (val, ref hc, ref idx);
				}
			}
			FieldInfo[] fields = type.GetFields (BindingFlags.Public | BindingFlags.Instance);
			foreach (FieldInfo field in fields)
			{
				Object[] attrs = field.GetCustomAttributes (typeof (SimpleEqualsAndHashcodeAttribute), true);
				if (attrs.Length != 0)
				{
					SimpleEqualsAndHashcodeAttribute attr = (SimpleEqualsAndHashcodeAttribute) attrs[0];
					if (!attr.UseForHashCode) continue;
					Object val = field.GetValue (obj);
					_hc (val, ref hc, ref idx);
				}
			}

			return hc;
		}

		/// <summary>
		/// Clones the given object using a Constructor on the object's type that 
		/// takes in an object of the same type
		/// </summary>
		/// <param name="obj1"></param>
		/// <returns></returns>
		public static Object CloneByConstructor (Object obj1)
		{
			if (obj1 == null) return null;
			Type type = obj1.GetType ();
			ConstructorInfo con = type.GetConstructor (new Type[] { type });
			if (con == null)
				throw new Exception (String.Format ("No cloning constructor found for type: \"{0}\"", type.FullName));
			Object obj2 = con.Invoke (new Object[] { obj1 });
			return obj2;
		}

		/// <summary>
		/// Clones the given object using the default Constructor on the object's type
		/// and copying the properties decorated with SimpleEqualsAndHashcodeAttribute
		/// </summary>
		/// <param name="obj1"></param>
		/// <returns></returns>
		public static Object Clone (Object obj1)
		{
			if (obj1 == null) return null;
			Type type = obj1.GetType ();
			ConstructorInfo con = type.GetConstructor (new Type[] { });
			if (con == null)
				throw new Exception (String.Format ("Class {0} does not have a default constructor", type.FullName));
			Object obj2 = con.Invoke (new Object[] { });
			PropertyInfo[] props = type.GetProperties (BindingFlags.Public | BindingFlags.Instance);
			foreach (PropertyInfo prop in props)
			{
				Object[] attrs = prop.GetCustomAttributes (typeof (SimpleEqualsAndHashcodeAttribute), true);
				if (attrs.Length != 0)
				{
					SimpleEqualsAndHashcodeAttribute attr = (SimpleEqualsAndHashcodeAttribute) attrs[0];
					if (!attr.UseForClone) continue;
					Object val1 = prop.GetValue (obj1, null);
					if (val1 is ICloneable)
					{
						val1 = ((ICloneable) val1).Clone ();
					}
					prop.SetValue (obj2, val1, null);
				}
			}
			return obj2;
		}

		/// <summary>
		/// Copies properties from the source object to the target object,
		/// if the properties are decorated with SimpleEqualsAndHashcodeAttribute.UseForClone == true
		/// </summary>
		/// <param name="target"></param>
		/// <param name="source"></param>
		/// <returns></returns>
		public static Object Copy (Object target, Object source)
		{
			if (source == null) return null;
			if (target == null) throw new NullReferenceException ();

			Type sourceType = source.GetType ();
			Type targetType = target.GetType ();
			PropertyInfo[] sourceProps = sourceType.GetProperties (BindingFlags.Public | BindingFlags.Instance);
			foreach (PropertyInfo sourceProp in sourceProps)
			{
				Object[] attrs = sourceProp.GetCustomAttributes (typeof (SimpleEqualsAndHashcodeAttribute), true);
				if (attrs.Length != 0)
				{
					SimpleEqualsAndHashcodeAttribute attr = (SimpleEqualsAndHashcodeAttribute) attrs[0];
					if (!attr.UseForClone) continue;

					PropertyInfo targetProp = targetType.GetProperty (sourceProp.Name, BindingFlags.Public | BindingFlags.Instance);
					if (targetProp != null
						&& targetProp.PropertyType.Equals (sourceProp.PropertyType))
					{
						Object sourceVal = sourceProp.GetValue (source, null);
						if (sourceVal is ICloneable)
						{
							sourceVal = ((ICloneable) sourceVal).Clone ();
						}
						targetProp.SetValue (target, sourceVal, null);
					}
				}
			}

			return target;
		}
	}
}